package sys.knot.data.access;

import org.apache.commons.lang3.StringUtils;
import org.slf4j.Logger;
import sys.knot.configuration.ServiceConfiguration;
import sys.knot.model.*;

import javax.enterprise.context.RequestScoped;
import javax.inject.Inject;
import javax.inject.Named;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;

@Named
@RequestScoped
public class QueryRunner {

    @Inject
    Logger logger;

    public void runQuery(ServiceConfiguration serviceConfiguration, Indata indata, ResultConsumer resultConsumer) {
        ResultQueue resultQueue = new ResultQueue(100);
        Collection<QueryTask> queryTasks = getTasks(serviceConfiguration, indata, resultQueue);
        ExecutorService executorService = Executors.newFixedThreadPool(queryTasks.size());
        try {
            for (QueryTask queryTask : queryTasks) {
                executorService.submit(queryTask);
            }

            handleStreamingResults(resultQueue, resultConsumer, serviceConfiguration.getLimitResultRows());

            resultQueue.getWarnings().forEach(s -> resultConsumer.addWarning(s));
            resultQueue.getErrors().forEach(s -> resultConsumer.addError(s));

        } catch (InterruptedException e) {
            //TODO handle exception.
            logger.error(e.getMessage(), e);
        } catch (IOException e) {
            //TODO handle exception.
            logger.error(e.getMessage(), e);
        } finally {
            executorService.shutdown();
        }
    }

    private void handleStreamingResults(ResultQueue resultQueue, ResultConsumer resultConsumer, int limit) throws InterruptedException, IOException {
        int counter = 0;
        // run an extra iteration to know if we broke the limit.
        while (!resultQueue.isFinishedAndEmpty() && !resultQueue.isCancelled()) {
            ResultPart x = resultQueue.poll(100, TimeUnit.MILLISECONDS);
            if (x != null) {
                counter++;
                if (counter > limit) {
                    //TODO deadlock here if the buffer is full?
                    resultQueue.cancel();
                    resultConsumer.addWarning("The result was limited to the max number of rows: " + limit);
                } else {
                    resultConsumer.consume(x);
                }
            }
        }
    }

    private Collection<QueryTask> getTasks(ServiceConfiguration serviceConfiguration, Indata indata, ResultQueue resultQueue) {
        Collection<QueryTask> queryTasks = new ArrayList<>();
        for (QuerySpecification querySpecification : serviceConfiguration.getQuerySpecifications()) {
            List<Object> orderedParameters = validateAndGetParameters(indata, querySpecification);
            log(querySpecification, orderedParameters);

            for (NamedDataSource namedDataSource : querySpecification.getDataSources()) {
                queryTasks.add(new QueryTask(
                                namedDataSource,
                                querySpecification,
                                orderedParameters,
                                resultQueue)
                );
            }
        }
        return queryTasks;
    }

    private void log(QuerySpecification querySpecification, List<Object> orderedParameters) {
        String readableSql = querySpecification.getSql();

        for (Object parameter : orderedParameters) {
            readableSql = StringUtils.replaceOnce(readableSql, "?", parameter.toString());
        }
        logger.info("running query (no exact sql): " + readableSql + ", in datasources: " + querySpecification.getDataSources());
    }

    private List<Object> validateAndGetParameters(Indata indata, QuerySpecification querySpecification) {
        List<Object> result = new ArrayList<>();
        for (QueryParameter queryParameter : querySpecification.getOrderedQueryParameters()) {
            Object parameterValue = queryParameter.getValidatedParameterFromIndata(indata);
            result.add(parameterValue);
        }
        return result;
    }

}
